CSS Font-Size Keyword Converter

Technical Specification

Version: 0.7.0 Plugin Name: NewUnit Platform: Sigil E-book Editor Language: Python 3.6+ with tkinter

Overview

Sigil plugin that converts CSS font-size keywords (xx-small, x-small, small, medium, large, x-large, xx-large, smaller, larger) to em or rem units across CSS files, embedded <style> blocks, and inline style="" attributes.

Core Requirements

Conversion Targets

  1. CSS Files: External .css stylesheets
  2. Embedded Styles: <style> blocks in HTML/XHTML headers
  3. Inline Styles: style="" attributes on HTML elements

Conversion Patterns

Primary (Always Active)

  • Pattern: font-size: <keyword>;
  • Safe and reliable for all contexts

Secondary (Optional)

  • Pattern: font: <keyword> <font-family>; and variations
  • User must explicitly enable
  • Requires validation and preview workflow

Keyword Mappings (Defaults)

Keyword Default Value Default Unit
xx-small0.6em
x-small0.75em
small0.89em
medium1em
large1.2em
x-large1.5em
xx-large2em
smaller0.85em
larger1.15em

Each keyword independently configurable for numeric value (positive float) and unit type (em or rem).

Architecture

Entry Point

def run(bk):
    """
    Main plugin entry point called by Sigil
    
    Args:
        bk: Sigil book container object
    
    Returns:
        0 on success, -1 on error
    """

Key Components

1. Configuration Management

Storage Location: ~/.sigil/NewUnit/configs/

File Format: JSON

{
    "plugin": "FontSizeKeywordToEm",
    "version": "0.7.0",
    "name": "User Config Name",
    "description": "Optional description",
    "created": "2025-01-15T10:30:00",
    "config": {
        "xxsmall": "0.6",
        "xxsmall_unit": "em",
        "convert_font_shorthand": "true",
        "create_backup": "false",
        "backup_path": "/path/to/backup"
    }
}

Types:

  • Default preferences (stored in Sigil prefs)
  • Named configurations (JSON files)

Validation:

  • Folder existence and creation
  • Write permissions testing
  • Numeric value validation (positive floats only)

2. Comment Handling

CRITICAL: CSS comments must be removed before pattern matching to avoid false positives
def strip_css_comments(css_text):
    """
    Remove CSS comments and return text with placeholders
    
    Returns:
        (text_no_comments, placeholder_map)
    """
    placeholder_map = {}
    
    def save_comment(match):
        placeholder = f"___CSS_COMMENT_{uuid.uuid4().hex}___"
        placeholder_map[placeholder] = match.group(0)
        return placeholder
    
    text_no_comments = re.sub(r'/\*.*?\*/', save_comment, css_text, 
                              flags=re.DOTALL)
    return text_no_comments, placeholder_map
CRITICAL: Must use UUID-based placeholders, not sequential numbers, to prevent collision with existing CSS content.

3. Safety Detection

def is_unsafe_shorthand(css_line):
    """
    Detect potentially unsafe font shorthand patterns
    
    Args:
        css_line: CSS line containing font property
    
    Returns:
        True if unsafe, False if safe
    """
    # Remove comments for accurate checking
    line_no_comments = re.sub(r'/\*.*?\*/', '', css_line)
    
    # Extract ONLY the font property value
    font_match = re.search(r'\bfont\s*:\s*([^;{}"\']+)', 
                          line_no_comments, re.IGNORECASE)
    if not font_match:
        return False
    
    font_value = font_match.group(1)
    
    # Check for complex functions IN THE VALUE
    if re.search(r'\b(calc|var|min|max|clamp)\s*\(', 
                font_value, re.IGNORECASE):
        return True
    
    # Check for multiple slashes
    if font_value.count('/') > 1:
        return True
    
    # Check for excessive complexity
    parts = re.split(r'\s+', font_value.strip())
    if len(parts) > 8:
        return True
    
    return False
Why check value only: Comments or other properties might contain these keywords, but we only care about the font value itself.

4. Backup System

Memory Backup (Always Enabled):

backup_files = {}
for file_info in selected_files:
    original_text = bk.readfile(file_info['id'])
    if not isinstance(original_text, str):
        original_text = original_text.decode('utf-8')
    backup_files[file_info['id']] = {
        'content': original_text,
        'href': file_info['href']
    }

External Backup (Optional):

if create_backup:
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    for file_info in selected_files:
        original_text = bk.readfile(file_info['id'])
        base_name = os.path.basename(file_info['href'])
        name, ext = os.path.splitext(base_name)
        backup_filename = f"{name}_{timestamp}{ext}"
        backup_filepath = os.path.join(backup_path, backup_filename)
        
        with open(backup_filepath, 'w', encoding='utf-8') as f:
            f.write(original_text)

UI Specifications

1. File Selector Window (Main)

Dimensions: Responsive

  • Minimum: 700x550
  • Maximum: 60% screen width, 70% screen height
  • Resizable: Yes
┌─────────────────────────────────────────────┐ │ [⚙ Configuration] │ ├─────────────────────────────────────────────┤ │ Filter: ◯ All ◯ HTML/XHTML ◯ CSS │ ├─────────────────────────────────────────────┤ │ Select files to convert: │ │ ┌─────────────────────────────────────────┐ │ │ │ ☑ style.css (CSS) - 5 match(es) │ │ │ │ ☑ page.html (TEXT) - 3 match(es) │ │ │ │ ☑ chapter1.html (TEXT) - 2 match(es) │ │ │ │ │ │ │ └─────────────────────────────────────────┘ │ │ Status: Ready │ ├─────────────────────────────────────────────┤ │ [Select All] [Clear] [Preview] [Apply] ... │ └─────────────────────────────────────────────┘

2. Configuration Window

Dimensions: Responsive (Minimum: 750x800, Maximum: 80% screen width/height)

┌─────────────────────────────────────────────────────┐ │ Configure Conversion Values │ ├─────────────────────────────────────────────────────┤ │ Configuration Management │ │ [Save as Default] │ │ [Save to Named Config] [Load Named Config] │ ├──────────────────────┬──────────────────────────────┤ │ LEFT COLUMN │ RIGHT COLUMN │ │ │ │ │ Set value and unit: │ Font Shorthand (Advanced) │ │ │ ☐ Convert font: shorthand │ │ Keyword Value em rem│ WARNING: Complex. Preview │ │ xx-small [0.6] ◯ ◯ │ shown before applying. │ │ x-small [.75] ◯ ◯ │ │ │ small [.89] ◯ ◯ │ Backup Options │ │ medium [ 1 ] ◯ ◯ │ ☐ Create backup copies │ │ large [1.2] ◯ ◯ │ Backup folder: │ │ x-large [1.5] ◯ ◯ │ [________________] [Browse] │ │ xx-large [ 2 ] ◯ ◯ │ Files saved with timestamp: │ │ smaller [.85] ◯ ◯ │ filename_YYYYMMDD_HHMMSS │ │ larger [1.15] ◯ ◯ │ │ │ │ │ │ Set all to: │ │ │ [All em] [All rem] │ │ └──────────────────────┴──────────────────────────────┘ │ [Error messages shown here in red] │ ├─────────────────────────────────────────────────────┤ │ [Reset Defaults] [Cancel] [Save] │ └─────────────────────────────────────────────────────┘

3. Preview Window

Dimensions: 800x600 (fixed)

Color Coding

Element Background Text Notes
Regular "before"#ffd6d6 (pink)#000000Font-size changes
Shorthand "before"#cce5ff (blue)#000000With [SHORTHAND] prefix
[SHORTHAND] labelsame as line#FF0000 (red)Bold font
"after" (all)#d6ffd6 (green)#000000Both types
File headersnone#2196F3 (blue)Bold, larger font

Conversion Workflow

Phase 1: Initialization

  1. Load preferences from Sigil
    • Get saved configuration values
    • Use defaults for missing values
    • Store in config dictionary
  2. Build regex patterns from config
  3. Scan all CSS and text files
  4. Display file selector window

Phase 2: User Selection

  1. Show file selector with checkboxes
  2. If Preview clicked: Build and show preview
  3. If Apply clicked: Continue to Phase 3

Phase 3: Backup

Memory Backup: Always enabled for undo functionality
External Backup: Optional, with folder validation

Phase 4: Conversion

For each selected file:

  1. Retrieve original from memory backup
  2. Strip CSS comments
  3. Apply patterns
  4. Restore CSS comments
  5. Write modified content
  6. Track changes and errors

Phase 5: Results & Undo

Display summary and enable undo functionality to restore from memory backup

Regular Expression Patterns

Font-Size Pattern

pattern = rf'(font-size\s*:\s*){re.escape(keyword)}(\s*(?=[;}}"\'\!]|$))'

Example transformation:

Input:  font-size: small;
Output: font-size: 0.89em;

Font Shorthand Pattern (If Enabled)

pattern = rf'(\bfont\s*:\s*(?:[^\s;{{}}]+\s+)*?){re.escape(keyword)}(\s*(?:[/\s][^\s;{{}}]+)*\s*(?=[;}}"\']|$))'

Example transformations:

Input:  font: small Arial;
Output: font: 0.89em Arial;

Input:  font: bold small/1.5 Georgia;
Output: font: bold 0.89em/1.5 Georgia;

Pattern Flags

Both patterns use: re.IGNORECASE | re.MULTILINE

Test Cases

Straightforward Cases (Should Convert)

.test-small { font-size: small; }
.compact{font-size:small;}
.uppercase { font-size: SMALL; }
.important { font-size: small !important; }

Font Shorthand Safe (If Enabled)

.basic { font: small Arial; }
.style { font: italic small serif; }
.weight { font: bold small monospace; }
.line-height { font: small/1.5 Arial; }

Font Shorthand Unsafe (Should Skip)

.calc { font: calc(1em + 2px) small Arial; }
.var { font: var(--my-size) small Arial; }
.min { font: min(12px, 1em) small Arial; }
.clamp { font: clamp(12px, 1em, 20px) small Arial; }

Should NOT Convert

/* Wrong properties */
.bg { background-size: small; }

/* Already converted */
.px { font-size: 12px; }
.em { font-size: 1em; }

/* System keywords */
.inherit { font-size: inherit; }

/* In comments */
/* font-size: small; */

Critical Implementation Notes

1. UUID-Based Placeholders

CRITICAL: Must use uuid.uuid4().hex, not sequential numbers.

Why: Sequential numbers like ___COMMENT_0___ could exist in CSS. Collision would corrupt CSS during restoration.

2. Regex Keyword Escaping

CRITICAL: Always re.escape() keywords before inserting into regex.

Why: Future-proofs against adding unusual keywords and is standard security practice.

3. Comment Scope in is_unsafe_shorthand()

CRITICAL: Extract and check ONLY the font property value, not the entire line.

Why: Comments might contain keywords like calc, var. Only care about what's inside the font property value.

4. Dialog Workflow Order

CRITICAL: Unsafe → Safe shorthand → Apply (in that order).

Why: User sees warnings before making decisions. Separates acknowledgment from choice.

5. Backup Priority

CRITICAL: Memory backup ALWAYS happens; external backup is optional.

Why: Memory backup enables undo within session. External backup provides crash recovery.

Dependencies

Required

  • Python: 3.6+ (f-strings used)
  • Standard Library: re, sys, os, json, uuid, datetime, tkinter
  • No third-party dependencies - Plugin is self-contained

Sigil API

Method Description
bk.getPrefs()Returns dict
bk.savePrefs(dict)Saves dict
bk.css_iter()Returns iterator of (id, href)
bk.text_iter()Returns iterator of (id, href)
bk.readfile(id)Returns bytes or str
bk.writefile(id, str)Writes str to file

Compatibility: Sigil 1.0+